package net.sf.odinms.exttools.doclet;

import java.io.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.LanguageVersion;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.RootDoc;
import com.sun.tools.doclets.internal.toolkit.util.*;
import net.sf.odinms.exttools.doclet.pages.*;

/**
 * A concrete subclass of Doclet for creating XHTML documentation. Since
 * AbstractDoclet prohibits usage by any class but HtmlDoclet, it is not used as
 * the parent class. However, this conforms to AbstractDoclet in case that
 * should ever change.
 */
@SuppressWarnings("unchecked")
public class Doclet extends com.sun.javadoc.Doclet {
	
	/** The global configuration information for this run of Javadoc. */
	private Config conf;
	
	/** The current version of XHTML Doclet. */
	public static final String DOCLET_VERSION = "0.4";
	
	/** The Subversion revision that was used to build this version. */
	public static final String DOCLET_REVISION =
			Config.getInstance().getDocletSpecificRevision();
	
	/** The date that this version was most recently updated. */
	public static final String DOCLET_BUILD_DATE =
			Config.getInstance().getDocletSpecificBuildDate();
	
	/** The size to use for byte buffers when copying resource files. */
	public static final int BUFFER_SIZE = 1024;
	
	/**
	 * Constructs a XhtmlDoclet object from which all XHTML Javadocs originate.
	 * Keep track of the program {@link RootDoc}, sets internal options based
	 * on execution flags, and print the doclet's build date information. NOTE:
	 * Any runtime exception encountered is propagated up call tree.
	 * 
	 * @param root An object representing the root of parsing the code.
	 */
	protected Doclet(RootDoc root) {
		conf = Config.getInstance();
		conf.root = root;
		conf.setOptions();
		System.out.println(conf.propertyText("doclet.info", DOCLET_VERSION,
				DOCLET_REVISION, DOCLET_BUILD_DATE, Config.JAVA_VERSION));
		System.out.println("      http://xhtmldoclet.sourceforge.net/");
		
		// TODO Resolve user-specified initial page and extension
		// (base filename is hard-coded in frameset, too)
		conf.topFile = "overview-summary" + conf.ext;
	}
	
	/**
	 * The "start" method to begin execution of the doclet, required by Javadoc.
	 * 
	 * @param root The root of the documentation tree.
	 * @see com.sun.javadoc.RootDoc
	 * @return true if the doclet ran without encountering any errors, false
	 *         otherwise.
	 */
	public static boolean start(RootDoc root) {
		try {
			new Doclet(root).generateDocumentation();
		}
		catch (IOException exc) {
			exc.printStackTrace();
			return false;
		}
		return true;
	}
	
	/**
	 * Create a doclet exception from an exception relating to a given filename.
	 * 
	 * @param exc The exception that was encountered and passed in.
	 * @param file The file being created when the exception happened.
	 * @return A {@link DocletAbortException} that can be thrown as needed.
	 */
	public static DocletAbortException exception(Exception exc, String file) {
		Config.getInstance().standardmessage.error(
				"doclet.exception_encountered", exc.toString(), file);
		return new DocletAbortException();
	}
	
	/**
	 * Indicate that this doclet supports the 1.5 language features.
	 * 
	 * @return JAVA_1_5, indicating that the new features are supported.
	 */
	public static LanguageVersion languageVersion() {
		return LanguageVersion.JAVA_1_5;
	}
	
	/**
	 * Find expected number of arguments for a given doclet option.
	 * 
	 * @param option The name of the argument for which to find the length.
	 * @return Number of arguments for the specified option. Zero means option
	 *         not known. Negative value means error occurred.
	 */
	public static int optionLength(String option) {
		// Construct temporary configuration for check
		return (Config.getInstance()).optionLength(option);
	}
	
	/**
	 * <p>
	 * Check that options have the correct and valid arguments.
	 * </p>
	 * 
	 * @param options A 2-dimensional array of options to check for validity.
	 *        Each of the first-order elements represents a single option, and
	 *        contains an array with the arguments corresponding to it.
	 * @param reporter An object for printing errors, notices, and warnings.
	 * @return true if the options are valid.
	 */
	public static boolean validOptions(String[][] options,
			DocErrorReporter reporter) {
		// Construct temporary configuration for check
		return (Config.getInstance()).validOptions(options, reporter);
	}
	
	/**
	 * Drives the generation of Javadoc files. Calls <code>generate()</code>
	 * methods in the individual writers, which generate the documentation
	 * files. Create a {@link ClassTree} first to ensure the class hierarchy is
	 * built first, then proceed to create all the pages specified by the user.
	 * Also copies all necessary image and CSS resource files to the directory
	 * where documentation is being created.
	 * 
	 * @throws IOException If an IO error occurs when copying resource files.
	 * @see com.sun.javadoc.RootDoc
	 */
	private void generateDocumentation() throws IOException {
		if (conf.root.classes().length == 0) {
			conf.message.error("No_Public_Classes_To_Document");
			return;
		}
		
		System.out.println(conf.propertyText("doclet.doc_root",
				conf.destDirName));
		
		// Build tree and index for all the packages and classes
		conf.classTree = new ClassTree(conf, conf.nodeprecated);
		conf.indexBuilder = new IndexBuilder(conf, conf.nodeprecated);
		conf.rootIndexPage =
				(conf.splitindex)
						? "index-files" + File.separatorChar + "index-"
								+ conf.indexBuilder.elements()[0] + conf.ext
						: "index-all" + conf.ext;
		if (conf.use) {
			System.out.println(conf.propertyText("doclet.mapping"));
			conf.classUseMapper = new ClassUseMapper(conf.root, conf.classTree);
		}
		
		// Copy CSS style sheet information, either user-specified or default
		if (!conf.stylesheetfile.equals(""))
			copyFileToFile(conf.stylesheetfile, "stylesheet.css");
		else {
			copyResourceFileToFile("resources", "core_styles.css", null,
					"stylesheet.css", false);
			// If -plainindex isn't used, add CSS & copy images for fancy index.
			if (!conf.plainindex) {
				copyResourceFileToFile("resources", "icon_styles.css", null,
						"stylesheet.css", true);
				System.out.println(conf.propertyText("doclet.images"));
				
				copyImageFilesToDestination();
			}
		}
		// TODO Modify CSS to hide non-generated Help sections
		
		// Copy image used in inheritance hierarchy diagrams
		copyResourceFileToFile("images", "inherit.gif", "image-files", null,
				false);
		
		System.out.println(conf.propertyText("doclet.common"));
		
		// Generate "package-list", a plaintext listing of included packages
		PackageList.generatePackageList();
		
		// Generate "index.html" with frameset appropriate to number of packages
		Frameset.generateFrameset();
		if (conf.packages.length > 1) {
			// Generate "overview-{frame | summary}.html" if 2+ packages
			AllPackages.generateAllPackages();
			OverviewSummary.generateOverviewSummary();
		}
		OverviewTree.generateOverviewTree();
		AllClasses.generateAllClasses();
		ConstantValues.generateConstantValues();
		SerializedForm.generateSerializedForm();
		DeprecatedList.generateDeprecatedList();
		AbstractIndex.generateIndex();
		Help.generateHelp();
		
		// Generate "package-{ frame | summary | tree | use }.html"
		System.out.println(conf.propertyText("doclet.packages"));
		generatePackageFiles();
		
		System.out.println(conf.propertyText("doclet.classes"));
		generateClassFiles();
		
		// Copy included doc-files directories to javadoc output directory
		if (conf.sourcepath != null && conf.sourcepath.length() > 0) {
			StringTokenizer pathTokens =
					new StringTokenizer(conf.sourcepath,
							String.valueOf(File.pathSeparatorChar));
			boolean first = true;
			while (pathTokens.hasMoreTokens()) {
				Util.copyDocFiles(conf,
						pathTokens.nextToken() + File.separator,
						DocletConstants.DOC_FILES_DIR_NAME, first);
				first = false;
			}
		}
		conf.tagletManager.printReport();
	}

	/**
	 * Copy image files to destination by JAR introspection or file listing.
	 * 
	 * @throws IOException If a call to {@link #copyResourceFileToFile} fails.
	 */
	private void copyImageFilesToDestination() throws IOException {
		// Introspect into JAR file, copy images to destination.
		try {
			JarFile jarFile = conf.enclosingJarFile();
			if (jarFile != null) {
				Enumeration<JarEntry> entries = jarFile.entries();
				while (entries.hasMoreElements()) {
					JarEntry entry = (JarEntry) entries.nextElement();
					String file = entry.getName();
					if (file.endsWith(".gif")) {
						copyResourceFileToFile("images",
								file.substring(file.lastIndexOf("/") + 1),
								"image-files", null, false);
					}
				}
			}
		}
		catch (FileNotFoundException exc) {
			// If not a JAR, iterate through images directory.
			Class c = getClass();
			String codePath =
					c.getProtectionDomain().getCodeSource().getLocation().getPath();
			String codePkg = c.getPackage().getName().replace(".", "/");
			File imagesDir = new File(codePath + codePkg + "/images/");
			if (imagesDir != null && imagesDir.listFiles() != null) {
				for (File file : imagesDir.listFiles()) {
					copyResourceFileToFile("images", file.getName(),
							"image-files", null, false);
				}
			}
		}
	}
	
	/**
	 * <p>
	 * Generate the package documentation. For each package, creates:
	 * </p>
	 * <ul>
	 * <li>"package-frame.html" (lower left frame)</li>
	 * <li>"package-summary.html" (main frame)</li>
	 * <li>"package-use.html" detailing usage of package members</li>
	 * <li>"package-tree.html" showing package member inheritance</li>
	 * </ul>
	 */
	private void generatePackageFiles() {
		PackageDoc[] packages = conf.packages;
		PackageDoc next, prev = null;
		
		for (int i = 0; i < packages.length; i++) {
			// System.out.println("Package: " + packages[i].name());
			next =
					(i + 1 < packages.length && packages[i + 1].name().length() > 0)
							? packages[i + 1]
							: null;
			// If the next package is unnamed package, skip 2 ahead if possible
			if (next == null && i + 2 < packages.length)
				next = packages[i + 2];
			PackageFrame.generatePackageFrame(packages[i]);
			PackageSummary.generatePackageSummary(packages[i], prev, next);
			if (conf.createtree)
				PackageTree.generatePackageTree(packages[i], prev, next);
			if (conf.use)
				PackageUse.generatePackageUse(packages[i], prev, next);
			prev = packages[i];
		}
	}
	
	/**
	 * Generate class documentation for packages and singly-specified classes.
	 */
	private void generateClassFiles() {
		// Generate files for single classes specified on the command line.
		for (String packageName : conf.classDocCatalog.packageNames())
			generateClassFiles(Arrays.asList(conf.classDocCatalog.allClasses(packageName)));
		// Generate files for package names specified on the command line.
		for (PackageDoc packageDoc : conf.root.specifiedPackages())
			generateClassFiles(Arrays.asList(packageDoc.allClasses()));
	}
	
	/**
	 * Generate class documentation, both summary and usage (if desired). Checks
	 * config to see if usage has been requested before creating usage pages.
	 * 
	 * @param classList A {@link List} of classes to document.
	 */
	private void generateClassFiles(List<ClassDoc> classList) {
		if (classList.size() == 0)
			return;
		Iterator classes;
		ClassDoc curr, prev, next;
		Collections.sort(classList);
		// Generate class summary pages
		classes = classList.iterator();
		prev = null;
		curr = (ClassDoc) classes.next();
		while (curr != null) {
			next = (classes.hasNext()) ? (ClassDoc) classes.next() : null;
			ClassSummary.generateClassSummary(curr, prev, next);
			prev = curr;
			curr = next;
		}
		// Generate class usage pages
		if (!conf.use)
			return;
		classes = classList.iterator();
		prev = null;
		curr = (ClassDoc) classes.next();
		while (curr != null) {
			next = (classes.hasNext()) ? (ClassDoc) classes.next() : null;
			ClassUse.generateClassUse(curr, prev, next);
			prev = curr;
			curr = next;
		}
	}
	
	/**
	 * Copy a specified resource file to the destination directory. Chooses
	 * whether to overwrite or append based on the <code>append</code> param.
	 * The document root path is prepended to the <code>dst</code> parameter
	 * to prevent unintentional writing outside of it. Output paths should be
	 * relative to the document root to avoid deeply nested folders.
	 * 
	 * @param srcPath The relative path from this file to the resource file.
	 * @param srcFile The name of the resource file to read.
	 * @param dstPath The relative path from the output document root.
	 * @param dstFile The name with which to write the file.
	 * @param append Whether the file to be written should be in append mode.
	 * @throws IOException If an error occurs during reading or writing files.
	 */
	private void copyResourceFileToFile(String srcPath, String srcFile,
			String dstPath, String dstFile, boolean append) throws IOException {
		if (dstFile == null)
			dstFile = srcFile;
		String destDir = conf.destDirName + ((dstPath != null) ? dstPath : "");
		DirectoryManager.createDirectory(conf, destDir);
		InputStream stream =
				getClass().getResourceAsStream(
						srcPath + "/" + srcFile);
		if (stream != null) {
			FileOutputStream out = new FileOutputStream(new File(destDir, dstFile), append);
			byte[] buf = new byte[BUFFER_SIZE];
			int len;
			while ((len = stream.read(buf)) > 0) {
				out.write(buf, 0, len);
			}
			stream.close();
			out.close();
		}
	}
	
	/**
	 * Copies src file to dst file. If the dst file does not exist, it is
	 * created. The document root path is prepended to the <code>dst</code>
	 * parameter to prevent unintentional writing outside of it. Output paths
	 * should be relative to the document root to avoid deeply nested folders.
	 * 
	 * @param src File from which to copy.
	 * @param dst Destination filename for the copy.
	 * @throws IOException May be thrown on a read/write error.
	 */
	private void copyFileToFile(String src, String dst) throws IOException {
		InputStream in = new FileInputStream(new File(src));
		OutputStream out =
				new FileOutputStream(new File(conf.destDirName, dst));
		byte[] buf = new byte[BUFFER_SIZE];
		int len;
		while ((len = in.read(buf)) > 0) {
			out.write(buf, 0, len);
		}
		in.close();
		out.close();
	}
	
}
